/*
 * Copyright 2008 Sun Microsystems, Inc.  All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Founadation.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
 * CA 95054 USA or visit www.sun.com if you need additional information or
 * have any questions.
 */
package com.sun.lwuit.html;

import com.sun.lwuit.Button;
import com.sun.lwuit.CheckBox;
import com.sun.lwuit.ComboBox;
import com.sun.lwuit.Command;
import com.sun.lwuit.Component;
import com.sun.lwuit.Container;
import com.sun.lwuit.Display;
import com.sun.lwuit.Font;
import com.sun.lwuit.Form;
import com.sun.lwuit.Graphics;
import com.sun.lwuit.Label;
import com.sun.lwuit.List;
import com.sun.lwuit.RadioButton;
import com.sun.lwuit.TextArea;
import com.sun.lwuit.TextField;
import com.sun.lwuit.events.ActionEvent;
import com.sun.lwuit.events.ActionListener;
import com.sun.lwuit.geom.Dimension;
import com.sun.lwuit.layouts.BorderLayout;
import com.sun.lwuit.layouts.BoxLayout;
import com.sun.lwuit.layouts.FlowLayout;
import com.sun.lwuit.plaf.Border;
import com.sun.lwuit.plaf.UIManager;
import com.sun.lwuit.table.TableLayout.Constraint;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

/**
 * HTMLComponent is a LWUIT Component that renders HTML documents that conform to the XHTML Mobile Profile 1.0
 *
 * @author Ofir Leitner
 */
public class HTMLComponent extends Container implements ActionListener {

    /**
     * If true a full screen width will be assumed, this helps to make less internal components as text is aggregated to long labels
     * If false, then the width is flexible, but every word will be rendered as a separate label.
     * Note that if false, RTL texts will display in
     */
    private static boolean FIXED_WIDTH = true;

    /**
     * A constant that can be used to obfuscate out HTMLInputFormat if unnecessary
     */
    static final boolean SUPPORT_INPUT_FORMAT = true;

    /**
     * If true, when a page is requested, the previous page is immediately removed, thus helping conserve memory (Since only 1 page is held in memory simultanously)
     * In next releases a better and external control over this will be provided.
     */
    private static final boolean CLEAN_ON_PAGE_REQUEST = false;


    // Indentation for various tags
    private static int INDENT_BLOCKQUOTE = 20;
    private static int INDENT_DD = 10;
    private static int INDENT_OL = 10; //Ordered list
    private static int INDENT_UL = 10; //Unordered list

    // The minimum number of visible items of a multiple combobox (Unless it has less)
    private static int MIN_MULTI_COMBOBOX_ITEMS = 4;
    private static int MAX_MULTI_COMBOBOX_ITEMS = 6;

    /**
     * The default size (in characters) of an input textfield
     */
    private static int DEFAULT_TEXTFIELD_SIZE = 20;

    /**
     * The default oolumns (in characters) of an textarea
     */
    private static int DEFAULT_TEXTAREA_COLS = 20;

    /**
     * The default rows (in characters) of an textarea
     */
    private static int DEFAULT_TEXTAREA_ROWS = 2;

    /**
     * Indicates a Justify alignment. Text justification is enabled only in FIXED_WIDTH mode
     */
    static final int JUSTIFY = 5;

    /**
     * The thickness of a line drawn by the HR tag
     */
    private static int HR_THICKNESS = 3;

    /**
     * The default font to use
     */
    private static HTMLFont DEFAULT_FONT = new HTMLFont(null,Font.createSystemFont(Font.FACE_SYSTEM, Font.STYLE_PLAIN, Font.SIZE_MEDIUM));

    /**
     * The color given to visited links
     */
    private static int COLOR_VISITED_LINKS = 0x990099;

    /**
     * A hashtable containing all defined HTMLFont objects
     */
    static Hashtable fonts = new Hashtable();

    // Constants for the various possible input types (i.e. allowed values of the INPUT tag)
     private static final int INPUT_CHECKBOX = 0;
     private static final int INPUT_HIDDEN = 1;
     private static final int INPUT_PASSWORD = 2;
     private static final int INPUT_RADIO = 3;
     private static final int INPUT_RESET = 4;
     private static final int INPUT_SUBMIT = 5;
     private static final int INPUT_TEXT = 6;
     private static final int INPUT_BUTTON = 7; //button has no use other than javascript, which is not supported
     private static final int INPUT_FILE = 8; //file upload is not supported
     private static final int INPUT_IMAGE = 9;

    /**
     * Defines the possible values for the type attribute in the input tag, ordered according to the INPUT_* constants
     */
    private static String[] INPUT_TYPE_STRINGS = {
            "checkbox","hidden","password","radio","reset",
            "submit","text","button", "file","image"
    };

    /**
     * Vector holding the possible input type strings.
     */
    private static Vector INPUT_TYPES = new Vector();

     // Ordered list types

     /**
      * Ordered list type legal identifiers as can be defined in the ATTR_TYPE of the OL_TAG
      */
     private static final char[] ORDERED_LIST_TYPE_IDENTIFIERS = {'1','A','a','I','i'};

     /**
     *  Numeric ordered list type (1 ,2, 3)
     */
    private static final int LIST_NUMERIC = 0;

    /**
     * Uppercase ordered list type (A, B, C)
     */
     private static final int LIST_UPPERCASE = 1;

     /**
      * Lowercase ordered list type (a, b, c)
      */
     private static final int LIST_LOWERCASE = 2;

     /**
      * Roman numerals uppercase ordered list type (I,II,III,IV,V ......)
      */
     private static final int LIST_ROMAN_UPPER = 3;

     /**
      * Roman numerals lowercase ordered list type (i, ii, iii, iv, v ...)
      */
     private static final int LIST_ROMAN_LOWER = 4;

     /**
      * Strings representing roman numerals 0-9
      */
     private static final String[] ROMAN_NUMERALS_ONES={"","I","II","III","IV","V","VI","VII","VIII","IX"};

     /**
      * Strings representing roman numeral tens (10,20,30 etc.)
      */
     private static final String[] ROMAN_NUMERALS_TENS={"","X","XX","XXX","XL","L","LX","LXX","LXXX","XC"};


     // Unordered list types
     /**
      * Indicates the disc style (full circle) for an unordered list
      */
     private static final int BULLET_DISC = 1;

     /**
      * Indicates the circle (empty circle) style for an unordered list
      */
     private static final int BULLET_CIRCLE = 2;

     /**
      * Indicates the square (full square) style for an unordered list
      */
     private static final int BULLET_SQUARE = 3;

     /**
      * Inidcates whether this component should load CSS files and consider STYLE tag and attributes when available..
      * Note that CSS requires to create significantlly more containers, as almost every tag needs to be in its own container
      * in order to manipulate style attributes like bgcolor, border etc. This is why the developer can disable CSS.
      * Currently this is a private variable, once CSS is actually available this will be exposed via public getter/setter.
      * Also there's no point setting this to true in this release as it will not lead to actual CSS, but just prepare containers for it.
      */
    private boolean loadCSS;// = true;
    int contCount=0; // debug for CSS

    /**
     * The default background color of an HTML document, can be changed with the BGCOLOR attribute in the BODY tag or via CSS
     */
    static private final int DEFAULT_BGCOLOR = 0xffffff;

    /**
     * The default color of text in HTML documents, can be changed with the TEXT attribute in the BODY tag or via CSS
     */
    static private final int DEFAULT_TEXT_COLOR = 0;

    /**
     * The default color of links in HTML documents, can be changed with the LINK attribute in the BODY tag or via CSS
     */
    static private final int DEFAULT_LINK_COLOR = 0x0000ff;


    // Document colors
    private int bgColor=DEFAULT_BGCOLOR;
    private int textColor=DEFAULT_TEXT_COLOR;
    private int linkColor=DEFAULT_LINK_COLOR;

    // Refrences to helper classes and delegates
    private DocumentRequestHandler handler; // The HTMLComponent's request handler
    private RedirectThread redirectThread; // A refrence to a redirection thread if exists
    private ImageThreadQueue threadQueue; // A reference to the ImageThreadQueue that handles asynchronous image download
    private HTMLCallback htmlCallback;

    // Page info and status
    private DocumentInfo docInfo; // The DocumentInfo object associated with this document
    private Element document; // The page DOM representation
    private String pageURL; // The current page's URL
    private int pageStatus=HTMLCallback.STATUS_NONE; // The page's status (See HTMLCallback)
    private boolean pageError; // true if there was an error during the page building process
    private String title; // The page's title (as obtained from the TITLE tag in the HEAD tag)
    private int displayWidth; // Used to store the display width to detect width changes later on.
    private boolean cancelled; // true if the page was cancelled (Signals all processing to stop)
    private boolean cancelledCaught; //true if the cancel signal was already caught (Used to avoid multiple cancel handling)
    private boolean showImages=true; //true to download image, false otherwise

    // Links related
    private Hashtable accessKeys = new Hashtable();// A hastable containing all the access keys in this document and their respective links
    private Hashtable anchors;// A hashtable containing all the anchors of this document
    private Hashtable inputFields; // A hashtable containing all the input fields in the page. Used for FOR labels
    Component firstFocusable; // The first focusable link on the page

    //Font
    private HTMLFont defaultFont = DEFAULT_FONT; // The default font

    // The following variables are used when building the component from the document.
    // After the component construction they are not used (Basically they can all be considered as "temporary" variables,
    // but due to the recursive nature of the document building operation, they are defined as members)

    //Containers

    /**
     * The main container is the top-level container to which other containers are added when building the page.
     * After the page was built the main container is added to the HTMLComponent.
     * We do not build directly on the HTMLComponent to allow the building process to be performed not in the EDT thread.
     */
    private Container mainContainer;

    /**
     * curContianer is the current container we are adding components to. AT the start it is the main container.
     * Later on it can be a certain table cell or a fieldset.
     */
    private Container curContainer;

    /**
     * curLine is the basic container to which components are added in the building process.
     * Components are added to it horizontally and when there no more space left (or a newline is needed) - it is added to the curContainer
     */
    private Container curLine;

    // Layouting - NOTE: currently this component assumes the full width of the screen and performs text wrapping due to a bug in the combination of FlowLayout inside BoxLayout. Once its fixed the variables x & width will not be necessary
    private int x; // Holds the horizontal position in the curLine container
    private int width; // Holds the width available to each line
    private int leftIndent; // Holds the current left margin (can be changed by tags)
    private boolean lastWasEmpty; // true if the last line was empty, used to prevent unneeded multiple row spacing

    // Font
    private HTMLFont font; // Holds the current used font (starts with defaultFont and changes according to various tags and attributes)

    // Links
    private String link; // The current link address (so when a text is processed, we know to attribute it to this link)
    private HTMLLink mainLink; // The "main" link. If a link spans over 2 or more lines, the first segment is considered the "main" link and is responsible for highlighting all segments when focused.
    private char accesskey='\0'; // The current accesskey for a link
    private String anchor; // The current page anchor
    private boolean linkVisited; // True if the current link address was visited

    // Forms
    private HTMLForm curForm; // The current HTMLForm to which input fields should be added
    private TextArea curTextArea; // The current TextArea
    private List curComboBox; // The current ComboBox
    private boolean optionTag; // true if an option tag is open
    private boolean optionSelected; //true if the current option is marked as selected (i.e. the default selection)
    private String optionValue; // The current option value

    // Lists
    private int ulLevel; //Unordered list level (Used to display the right bullet)
    private int olIndex; //Index of the current item in the list
    private Vector olUpperLevelIndex;// = new Vector(); //Used to save the index when nesting
    private int listType; // The type of the current list

    // Tables
    private Vector tables;// A vector used for nesting, when a table contains a table ,the current one is "pushed" into the vector, to be "popped" later when the nested table processing ends.
    private Vector tableCells;// A vector used for nesting of table cells
    private HTMLTableModel curTable; // The model of teh currently collected table

    // Misc. tags
    private Vector fieldsets; // A vector used to support FIELDSET tag nesting
    private int preTagCount=0; // A counter used to support PRE tag nesting
    private int quoteTagCount=0; // A counter used to support QUOTE tag nesting
    private String labelForID; // Collects <label for="id">

    private Vector containers=new Vector();

    /**
     * This static segment sets up the INPUT_TYPES vector with values from INPUT_TYPE_STRINGS.
     * This is used later on for lookup.
     */
    static {
        for(int i=0;i<INPUT_TYPE_STRINGS.length;i++) {
            INPUT_TYPES.addElement(INPUT_TYPE_STRINGS[i]);
        }
    }

    /**
     * Sets the given LWUIT font for use with HTMLComponents.
     *
     * The font key can contain information about the following attributes:<br/>
     *
     * * The font family - i.e. Arial, Times New Roman etc.<br/>
     * * The font size - in pixels (i.e. 12, 14 etc.)<br/>
     * * The font style - bold, italic or both (no need to specify plain)<br/>
     * * The font tag assignments - if this font should be used for any HTML specific tags they should be specified - i.e. h1, kbd etc.<br/>
     * <br/>
     * The key is just a concatenation of all attributes seperated with a dot.<br/>
     * Examples for valid keys:<br/>
     * arial.12 - Describes a plain font from the arial family in size 12<br/>
     * arial.16.bold - A bold arial font in size 16<br/>
     * courier.italic.bold.20 - A bold and italic courier font in size 20<br/>
     * arial.20.h1 - A plain arial font, size 20, that should be used for contents of the H1 tag<br/>
     * code.kbd.samp - A font that should be used for the CODE, KBD and SAMP tags<br/>
     * <br/>
     * Note that the order of the attributes is not important and also that the case is ignored.<br/>
     * This means that arial.12.bold.italic.h3 is equivalent to itALIc.H3.arial.BOLD.12<br/>
     * <br/>
     * Also note that while you do not have to provide all the info for the font, but the info helps the rendering engine to reuse fonts when suitable.
     * For example, if you have a 16px arial bold font which you want to use for H2, you can simply add it as "h2", but if you add it as "arial.16.bold.h2"
     * then if the current font is arial.16 and the renderer encounters a B tag, it will know it can use the font you added as the bold counterpart of the current font.<br/>
     * <br/>
     * When adding system fonts there is no need to describe the font, the usage of setFont with system fonts is usually just to assign them for tags.
     * The rendering engine knows to derive bold/italic/bigger/smaller fonts from other system fonts (default or tag fonts) even if not added.
     *      
     * @param fontKey The font key in the format described above
     * @param font The actual LWUIT font object
     */
    public static void addFont(String fontKey, Font font) {
        //TODO - validation at add time
        fonts.put(fontKey,new HTMLFont(fontKey.toLowerCase(),font));
    }

    /**
     * Adds the given symbol and code to the user defined char entities table.
     * Symbols do not need to include leading & and trailing ; - these will be trimmed if given as the symbol
     *
     * @param symbol The symbol to add
     * @param code The symbol's code
     */
    public static void addCharEntity(String symbol,int code) {
        Parser.addCharEntity(symbol, code);
    }

    /**
     * Adds the given symbols array  to the user defined char entities table with the startcode provided as the code of the first string, startcode+1 for the second etc.
     * Some strings in the symbols array may be null thus skipping code numbers.
     *
     * @param symbols The symbols to add
     * @param startcode The symbol's code
     */
    public static void addCharEntitiesRange(String[] symbols,int startcode) {
        Parser.addCharEntitiesRange(symbols, startcode);
    }

    /**
     * Sets the maximum number of threads to use for image download
     *
     * @param threadsNum the maximum number of threads to use for image download
     */
    public static void setMaxThreads(int threadsNum) {
        ImageThreadQueue.setMaxThreads(threadsNum);
    }

    /**
     * Constructs HTMLComponent
     * 
     * @param handler The HttpRequestHandler to which all requestes for links will be sent
     */
    public HTMLComponent(DocumentRequestHandler handler) {
        //setLayout(new BoxLayout(BoxLayout.Y_AXIS));
        setLayout(new BorderLayout());
        this.handler=handler;
        threadQueue=new ImageThreadQueue(this);
        setHandlesInput(true);
        setScrollableY(true);
        setScrollableX(false);
        setSmoothScrolling(true);

        //Create some default fonts
        HTMLFont italic=new HTMLFont(null,Font.createSystemFont(Font.FACE_SYSTEM, Font.STYLE_ITALIC, Font.SIZE_MEDIUM));
        HTMLFont monospace=new HTMLFont(null, Font.createSystemFont(Font.FACE_MONOSPACE, Font.STYLE_PLAIN, Font.SIZE_MEDIUM));

        //Assign default fonts to some tags
        fonts.put(Element.TAG_NAMES[Element.TAG_EM], italic);
        fonts.put(Element.TAG_NAMES[Element.TAG_STRONG], new HTMLFont(null,Font.createSystemFont(Font.FACE_SYSTEM, Font.STYLE_BOLD, Font.SIZE_MEDIUM)));
        fonts.put(Element.TAG_NAMES[Element.TAG_DFN], italic);
        fonts.put(Element.TAG_NAMES[Element.TAG_CODE], monospace);
        fonts.put(Element.TAG_NAMES[Element.TAG_SAMP], monospace);
        fonts.put(Element.TAG_NAMES[Element.TAG_KBD], monospace);
        fonts.put(Element.TAG_NAMES[Element.TAG_VAR], italic);
        fonts.put(Element.TAG_NAMES[Element.TAG_CITE], italic);
        fonts.put(Element.TAG_NAMES[Element.TAG_PRE], monospace);
        fonts.put(Element.TAG_NAMES[Element.TAG_H1], new HTMLFont(null, Font.createSystemFont(Font.FACE_SYSTEM, Font.STYLE_BOLD, Font.SIZE_LARGE)));
        fonts.put(Element.TAG_NAMES[Element.TAG_H2], new HTMLFont(null, Font.createSystemFont(Font.FACE_SYSTEM, Font.STYLE_ITALIC, Font.SIZE_LARGE)));
        fonts.put(Element.TAG_NAMES[Element.TAG_H3], new HTMLFont(null, Font.createSystemFont(Font.FACE_SYSTEM, Font.STYLE_BOLD, Font.SIZE_MEDIUM)));

    }

    /**
     * Returns the document request handler
     *
     * @return the document request handler
     */
    public DocumentRequestHandler getRequestHandler() {
        return handler;
    }

    /**
     * Returns the DocumentInfo that currently represents the document loaded/shown
     *
     * @return the DocumentInfo that currently represents the document loaded/shown
     */
    public DocumentInfo getDocumentInfo() {
        return docInfo;
    }

    /**
     * Sets an HTMLCallback to listen to this HTMLCOmponent
     * 
     * @param callback The HTMLCallback that will receive events
     */
    public void setHTMLCallback(HTMLCallback callback) {
        htmlCallback=callback;
    }

    /**
     * Returns the HTMLCallback that is set on this HTMLComponent
     *
     * @return the HTMLCallback that is set on this HTMLComponent or null if none
     */
    public HTMLCallback getHTMLCallback() {
        return htmlCallback;
    }


    /**
     * Sets the default font for this HTMLComponent
     * 
     * @param fontKey The font key in the format described in setFont
     * @param font The actual LWUIT font object
     * @see setFont
     */
    public void setDefaultFont(String fontKey, Font font) {
        defaultFont=new HTMLFont(fontKey, font);
        if (fontKey!=null) {
            fonts.put(fontKey.toLowerCase(),defaultFont);
        }
    }

    /**
     * Sets whether this HTMLComponent will download and show linked images or not
     * 
     * @param show true to show images, false otherwise
     */
    public void setShowImages(boolean show) {
        showImages=show;
    }


    /**
     * Scrolls the HTMLComponent several pixels forward/backward.
     *
     * @param pixels The number of pixels to scroll (positive for forward and negative for backward)
     * @param animate true to animate the scrolling, false otherwise
     */
    public void scrollPixels(int pixels,boolean animate) {
        int scrollToY=getScrollY()+pixels;
        scrollTo(scrollToY,animate);
    }

    /**
     * Scrolls the HTMLComponent several pages forward/backward.
     * TO scroll to the start or end of the document, one can provide a very big number.
     * 
     * @param pages The number of pages to scroll (positive for forward and negative for backward)
     * @param animate true to animate the scrolling, false otherwise
     */
    public void scrollPages(int pages,boolean animate) {
        int scrollToY=getScrollY()+getHeight()*pages;
        scrollTo(scrollToY,animate);

    }

    /**
     * Scrolls the HTMLComponent to the denoted Y position
     * @param y The Y-coordinate to scroll to
     * @param animate true to animate the scrolling, false otherwise
     */
    private void scrollTo(int y,boolean animate) {
        if (y<0) {
            y=0;
        } else if (y>getPreferredH()-getHeight()) {
            y=getPreferredH()-getHeight();
        }
        if (animate) {
            scrollRectToVisible(getX(), y, getWidth(), getHeight(), this);
        } else {
            setScrollY(y);
        }
    }

    /**
     * Sets the given string containing HTML code as this HTMLComponent's body
     *
     * @param htmlText The HTML body to set
     */
    public void setBodyText(String htmlText) {
        setBodyText(htmlText, null);
    }

    /**
     * Sets the given string containing HTML code as this HTMLComponent's body.<br/>
     * The string is read using the specified encoding. If the encoding is not supported it will be read without encoding
     *
     * @param htmlText The HTML body to set
     * @param encoding The encoding to use when reading the HTML i.e. UTF8, ISO-8859-1 etc.
     * @return true if the encoding succeeded, false otherwise
     */
    public boolean setBodyText(String htmlText,String encoding) {
        return setHTML(htmlText, encoding, null,false);
    }
    
    /**
     * Sets the given string containing HTML code either as this HTMLComponent's body or as the full HTML.<br/>
     * The string is read using the specified encoding. If the encoding is not supported it will be read without encoding
     *
     * @param htmlText The HTML to set
     * @param encoding The encoding to use when reading the HTML i.e. UTF8, ISO-8859-1 etc.
     * @param title The HTML title, or null if none (Used only when isFullHTML is false)
     * @param isFullHTML true if this is a full HTML document (with html/body tags), false if this HTML should be used as the HTMLComponent's body
     * @return true if the encoding succeeded, false otherwise
     */
    public boolean setHTML(String htmlText,String encoding,String title,boolean isFullHTML) {
        boolean success=true;
        InputStreamReader isr = getStream(htmlText, encoding, title ,isFullHTML);

        if (isr==null) {
            isr=getStream("Encoding error loading string", null, title, isFullHTML); //TODO - dynamic error messages
            success=false;
        }
        final InputStreamReader isReader=isr;

        new Thread() {
            public void run() {
                Element doc = Parser.getInstance().parse(isReader,htmlCallback);
                documentReady(null, doc);
            }
        }.start();

        return success;
    }

    /**
     * Convenience method that calls getStream(String,String,String,boolean)
     *
     * @param htmlText The HTML to set
     * @param encoding The encoding to use when reading the HTML i.e. UTF8, ISO-8859-1 etc.
     * @return A stream representing the HTML
     */
    private InputStreamReader getStream(String htmlText,String encoding) {
        return getStream(htmlText, encoding, null, false);
    }

    /**
     * Obtains a stream of the given HTML with the given encoding
     *
     * @param htmlText The HTML to set
     * @param encoding The encoding to use when reading the HTML i.e. UTF8, ISO-8859-1 etc.
     * @param title The HTML title, or null if none
     * @param isFullHTML true if this is a full HTML document (with html/body tags), false otherwise
     * @return A stream representing the HTML
     */
    private InputStreamReader getStream(String htmlText,String encoding,String title,boolean isFullHTML) {
        if (!isFullHTML) {
            String titleStr="";
            if (title!=null) {
                titleStr="<head><title>"+title+"</title></head>";
            }
            htmlText="<html>"+titleStr+"<body>"+htmlText+"</body></html>";
        }

        ByteArrayInputStream bais = new ByteArrayInputStream(htmlText.getBytes());
        InputStreamReader isr=null;
        if (encoding!=null) {
            try {
                isr = new InputStreamReader(bais,encoding);
            } catch (UnsupportedEncodingException uee) {
                uee.printStackTrace();
            }
        }
        if (isr==null) { //encoding wasn't specified or failed
            isr = new InputStreamReader(bais);
        }
        return isr;
    }

    /**
     * Cancels the loading of the current page
     */
    public void cancel() {
          cancelled=true;
          cancelRedirectsAndImages();
    }

    /**
     * Sets this HTMLComponent to render the document in the specified URL
     * 
     * @param pageURL The URL containing the HTML document
     */
    public void setPage(final String pageURL) {
        setPage(new DocumentInfo(pageURL));
    }

    /**
     * Sets this page to render the document specified in th DocumentInfo object
     * 
     * @param docInfo Containing info about the document (url, encoding etc)
     */
    void setPage(final DocumentInfo docInfo) {
        cancelRedirectsAndImages();
        if  ((pageStatus==HTMLCallback.STATUS_REQUESTED) ||
                (pageStatus==HTMLCallback.STATUS_CONNECTED) ||
                (pageStatus==HTMLCallback.STATUS_PARSED)) {  //previous page still loading
            cancel();

            // TODO - This mechanism is far from ideal - handle better page life cycle
            int waitTime=0;
            while ((pageStatus!=htmlCallback.STATUS_CANCELLED) && (waitTime<2500)) {
                System.out.println("Waiting for previous page to cancel "+System.currentTimeMillis());
                try { Thread.sleep(50); } catch (Exception e) { }
                waitTime+=50;

            }
        }
        cancelled=false;
        cancelledCaught=false;
        
        if (CLEAN_ON_PAGE_REQUEST) {
            cleanup();
            removeAll();
            repaint();
        }

        new Thread() {

            public void run() {
                InputStreamReader isr=null;
                setPageStatus(HTMLCallback.STATUS_REQUESTED);
                try {
                    InputStream is=handler.resourceRequested(docInfo);
                    if (is!=null) {
                        isr = new InputStreamReader(is,docInfo.getEncoding());
                    }
                } catch (UnsupportedEncodingException uee) {
                    boolean cont=true;
                    if (htmlCallback!=null) {
                        cont=htmlCallback.parsingError(HTMLCallback.ERROR_ENCODING,null ,null,null,"Page encoding not supported - "+docInfo.getEncoding());
                    }
                    if (cont) { //retry without the encoding
                        isr = new InputStreamReader(handler.resourceRequested(docInfo));
                    } else {
                        isr = getStream("Page encoding not supported", null);
                        setPageStatus(HTMLCallback.STATUS_ERROR);
                    }
                }

                if (cancelled) {
                    isr=getStream("Page loading cancelled by user",null);
                    setPageStatus(HTMLCallback.STATUS_CANCELLED);
                }

                if (isr==null) {
                    if (htmlCallback!=null) {
                        htmlCallback.parsingError(HTMLCallback.ERROR_CONNECTING, null, null, null, "Error connecting to stream");
                    }
                    setPageStatus(HTMLCallback.STATUS_ERROR);
                    isr = getStream("Error connecting to stream", null); //TODO - dynamic error messages
                } else {
                    setPageStatus(HTMLCallback.STATUS_CONNECTED);
                }

                Element newDoc=null;
                
                try {
                    newDoc=Parser.getInstance().parse(isr,htmlCallback);
                } catch (IllegalArgumentException iae) {
                    iae.printStackTrace();
                    setPageStatus(HTMLCallback.STATUS_ERROR);
                    isr = getStream("Parsing error "+iae.getMessage(), null);
                    newDoc=Parser.getInstance().parse(isr,htmlCallback);
                }

                if (cancelled) {
                    isr=getStream("Page loading cancelled by user",null);
                    newDoc=Parser.getInstance().parse(isr,htmlCallback);
                    setPageStatus(HTMLCallback.STATUS_CANCELLED);
                }


                setPageStatus(HTMLCallback.STATUS_PARSED);
                documentReady(docInfo, newDoc);
            }
        }.start();
    }

    /**
     * Called internally after both reading and parsing of the HTML document has completed
     * 
     * @param pageURL The URL of the page
     * @param newDocument An element which is the root of the document
     */
    void documentReady(DocumentInfo docInfo,Element newDocument) {
        
        this.pageURL=null;
        if (docInfo!=null) {
            this.pageURL=docInfo.getUrl();
        }
        this.docInfo=docInfo;
        document=newDocument;
        cleanup();
        rebuildPage();
        
        if ((!cancelled) || (cancelledCaught)) {
            Display.getInstance().callSerially(new Runnable() {

                public void run() {
                        removeAll();
                        addComponent(BorderLayout.CENTER,mainContainer);
                        setScrollY(0);

                        revalidate();
                        repaint();

                        //Places the focus on the first link in the page, as long as it is within the visible area of the first page
                        if (getComponentForm()!=null) {
                            if ((firstFocusable!=null) && (firstFocusable.getY()<getHeight())) {
                                getComponentForm().setFocused(firstFocusable);
                            } else {
                                getComponentForm().setFocused(mainContainer);
                            }
                        }

                        setPageStatus(HTMLCallback.STATUS_DISPLAYED);
                        if ((!showImages) || (threadQueue.getQueueSize()==0)) {
                            setPageStatus(HTMLCallback.STATUS_COMPLETED);
                        } else {
                            threadQueue.startRunning();
                        }

                        if (pageURL!=null) { // pageURL can be null if the page was set using setBodyText and not setPage
                            int hash=pageURL.indexOf('#');

                            if ((hash!=-1) && (pageURL.length()>hash+1)) { // URL contains an anchor
                                String anchorName=pageURL.substring(hash+1);
                                goToAnchor(anchorName);
                            }
                        }


                }
            });
        } else { // Page was cancelled
            setPageStatus(HTMLCallback.STATUS_CANCELLED);
            InputStreamReader isr=getStream("Page loading cancelled by user",null);
            Element newDoc=Parser.getInstance().parse(isr,htmlCallback);
            documentReady(docInfo, newDoc);
        }

    }


    /**
     * Returns the HTML page's title as described in its TITLE tag
     *
     * @return the HTML page's title as described in its TITLE tag
     */
    public String getTitle() {
        return title;
    }

    /**
     * Returns the page's URL
     * 
     * @return the current page's URL
     */
    public String getPageURL() {
        return pageURL;
    }

    /**
     * Returns the page status
     *
     * @return the page status (One of the STATUS_* constants in HTMLCallback)
     */
    public int getPageStatus() {
        return pageStatus;
    }

    /**
     * Sets the page status to the given one
     *
     * @param status The new page status
     */
    void setPageStatus(final int status) {
        if ((!pageError) || (status==HTMLCallback.STATUS_REQUESTED)) { //Once the page error flag has been raised the page status doesn't change until a new page is requested
            pageStatus=status;
            if (htmlCallback!=null) {
                if (Display.getInstance().isEdt()) {
                    htmlCallback.pageStatusChanged(this, status, pageURL);
                } else {
                    Display.getInstance().callSerially(new Runnable() {
                        public void run() {
                            htmlCallback.pageStatusChanged(HTMLComponent.this, status, pageURL);
                        }
                    });
                }
            }
            pageError=((status==HTMLCallback.STATUS_ERROR) || (status==HTMLCallback.STATUS_CANCELLED));
            cancelledCaught=(status==HTMLCallback.STATUS_CANCELLED);
        }
    }

    /**
     * A utility method to convert between Element TAG_* constants and HTMLFont font attribute constants.
     * 
     * @param tag The tag that describes the font attribute 
     * @param font The font for which we want a counterpart font
     * @return the counterpart font for the given font in teh specified tag sense.
     */
    private HTMLFont getCounterpartFont(int tag,HTMLFont font) {
        int attribute=-1;
        switch(tag) {
            case Element.TAG_B:
                attribute=HTMLFont.BOLD;
                break;
            case Element.TAG_I:
                attribute=HTMLFont.ITALIC;
                break;
            case Element.TAG_BIG:
                attribute=HTMLFont.BIG;
                break;
            case Element.TAG_SMALL:
                attribute=HTMLFont.SMALL;
                break;
        }
        if (attribute==-1) {
            return font;
        }

        HTMLFont cFont=font.getCounterpartFont(attribute);
        if (cFont!=null) {
             return cFont;
        }

        HTMLFont bestFit=null;
        Enumeration e=fonts.elements();
        while (e.hasMoreElements()) {
            HTMLFont hFont=(HTMLFont)e.nextElement();
            if (hFont.isCounterpart(attribute,font)) {
                if (attribute==HTMLFont.BIG) { //takes the closests font (i.e. big but not biggest)
                    if ((bestFit==null) || (bestFit.getSize()>hFont.getSize())) {
                        bestFit=hFont;
                    }
                } else if (attribute==HTMLFont.SMALL) {
                    if ((bestFit==null) || (bestFit.getSize()<hFont.getSize())) {
                        bestFit=hFont;
                    }
                } else { // BOLD, ITALIC
                    font.setCounterpartFont(attribute,hFont);
                    return hFont;
                }
            }
        }

        if (bestFit==null) {
            bestFit=font;
        }
        font.setCounterpartFont(attribute,bestFit);
        return bestFit;
    }

    /**
     * Rebuilds the HTMLComponent, this is called usually after a new page was loaded.
     */
    private void cleanup() {
        displayWidth=Display.getInstance().getDisplayWidth();

        //reset all building process values
        
        leftIndent=0;
        x=0;

        containers=new Vector();
        anchors=new Hashtable();
        anchor=null;
        
        accesskey='\0';
        for (Enumeration e=accessKeys.keys();e.hasMoreElements();) {
            int keyCode=((Integer)e.nextElement()).intValue();
            getComponentForm().removeKeyListener(keyCode,this);
        }
        accessKeys=new Hashtable();

        fieldsets=new Vector();
        
        curTable=null;
        tables=new Vector();
        tableCells=new Vector();
        
        ulLevel=0;
        olIndex=Integer.MIN_VALUE;
        olUpperLevelIndex=new Vector();
        listType=LIST_NUMERIC;
        
        font=defaultFont;

        labelForID=null;
        inputFields=new Hashtable();

        link=null;
        linkVisited=false;
        mainLink=null;
        firstFocusable=null;

        curForm=null;
        curTextArea=null;
        curComboBox=null;

        optionTag=false;
        optionSelected=false;

        preTagCount=0;
        quoteTagCount=0;

        mainContainer=new Container();
        mainContainer.setScrollableX(false);
        mainContainer.setLayout(new BoxLayout(BoxLayout.Y_AXIS));

        curContainer=mainContainer;
        curLine=new Container();
        lastWasEmpty=false;

        width=Display.getInstance().getDisplayWidth()-getStyle().getMargin(Component.LEFT)-getStyle().getPadding(Component.LEFT)-
                getStyle().getMargin(Component.RIGHT)-getStyle().getPadding(Component.RIGHT)-10; // The -10 is arbitrary to avoid edge cases
    }

    private void rebuildPage() {
        // Get the HTML root tag and extract the HEAD and BODY (Note that the document tag is ROOT which contains HTML and so on.
        Element html=document.getChildById(Element.TAG_HTML);
        Element body=null;
        Element head=null;
        if (html!=null) {
            String dir=html.getAttributeById(Element.ATTR_DIR);

            // TODO - Should be applyRTL, but it doesn't work, setRTL is problematic since it changes the direction of the whole application, and we only need to change the component
            UIManager.getInstance().getLookAndFeel().setRTL((dir!=null) && (dir.equals("rtl")));
            body=html.getChildById(Element.TAG_BODY);
            head=html.getChildById(Element.TAG_HEAD);
        }

        // Fetch the document's title
        title=null;
        if (head!=null) {
            Element baseTag=head.getChildById(Element.TAG_BASE);
            if (baseTag!=null) {
                String baseURL=baseTag.getAttributeById(Element.ATTR_HREF);
                if ((baseURL!=null) && (docInfo!=null)) {
                    docInfo.setBaseURL(baseURL);
                }
            }
            Element titleTag=head.getChildById(Element.TAG_TITLE);
            if (titleTag!=null) {
                Element titleText=titleTag.getChildById(Element.TAG_TEXT);
                if (titleText!=null) {
                    title=titleText.getAttributeById(Element.ATTR_TITLE);
                }
            }
        }

        if (htmlCallback!=null) {
            Display.getInstance().callSerially(new Runnable() {
                public void run() {
                    htmlCallback.titleUpdated(HTMLComponent.this, title);
                }
            });
        }

        if (body!=null) {
            bgColor=Element.getColor(body.getAttributeById(Element.ATTR_BGCOLOR),DEFAULT_BGCOLOR);
            textColor=Element.getColor(body.getAttributeById(Element.ATTR_TEXT),DEFAULT_TEXT_COLOR);
            linkColor=Element.getColor(body.getAttributeById(Element.ATTR_LINK),DEFAULT_LINK_COLOR);
            mainContainer.getStyle().setBgColor(bgColor);
            mainContainer.getStyle().setBgTransparency(255);
            processTag(body,Component.LEFT);
            if (loadCSS) {
                body.setAssociatedComponents(mainContainer);
            }
            newLine(Component.LEFT); //flush buffer
        } else {
            System.out.println("no BODY tag was found in page.");
        }

        if (!cancelled) {
            checkRedirect(head);
        }
    }

    /**
     * Checks if there's a refresh or redirect directive in the META tag and if so executes accordfingly.
     * 
     * @param head The HEAD element of the document
     */
    private void checkRedirect(Element head) {
        if (head!=null) {
            Element meta=head.getChildById(Element.TAG_META);
            if (meta!=null) {
                String httpequiv=meta.getAttributeById(Element.ATTR_HTTPEQUIV);
                if ((httpequiv!=null) && (httpequiv.equalsIgnoreCase("refresh"))) {
                    String content=meta.getAttributeById(Element.ATTR_CONTENT);
                    if (content!=null) {
                        int seperator=content.indexOf(';');
                        String redirectURL=null;
                        if (seperator!=-1) {
                            String tempUrl=content.substring(seperator+1);

                            redirectURL="";
                            for(int i=0;i<tempUrl.length();i++) {
                                char ch=tempUrl.charAt(i);
                                if (!Parser.isWhiteSpace(ch)) {
                                    redirectURL+=ch;
                                }
                            }
                            
                            if (redirectURL.startsWith("url=")) { // i.e. 10;url=http://....
                                redirectURL=redirectURL.substring(4);
                            }

                            content=content.substring(0, seperator);
                        }
                        int redirectTime=-1;
                        try {
                            redirectTime=Integer.parseInt(content);
                            redirectThread=new RedirectThread(this, redirectTime, redirectURL);
                            new Thread(redirectThread).start();
                        } catch (NumberFormatException nfe) {
                            //Wasn't a number - ignore tag
                        }

                    }
                }
            }
        }

    }

    /**
     * Cancels redirects if exists. This is useful when the page has a meta tag with redirection/refresh in X seconds, and in that time the user clicked a link
     */
    void cancelRedirectsAndImages() {
        if (redirectThread!=null) {
            redirectThread.cancel();
            redirectThread=null;
        }
        threadQueue.discardQueue();
    }

    /**
     * Adds the container representing the current line to the current container and creates a new one
     */
    private void newLine(int align) {
      if (curLine.getPreferredH()==0) {
          curLine.setPreferredH(font.getHeight());
      }
      lastWasEmpty=(curLine.getComponentCount()==0);
      curContainer.addComponent(curLine);
      curLine=new Container();
      curLine.getStyle().setBgTransparency(0);
      if (!FIXED_WIDTH) {
        curLine.setLayout(new FlowLayout(align));
      }
      curLine.setScrollableX(false);
      

      curLine.getStyle().setMargin(Component.LEFT,leftIndent);
      x=leftIndent;

    }

    /**
     * Same as newLine, but only if the current line container is not empty
     */
    private void newLineIfNotEmpty(int align) {
        if (curLine.getComponentCount()>0) {
            newLine(align);
        }
    }

    /**
     * Same as newLine, but only if the last line container was not empty
     */
    private void newLineIfLastWasNotEmpty(int align) {
        if (!lastWasEmpty) {
            newLine(align);
        }
    }

    /**
     * Shows a text that has been marked with the html PRE tag which means the text will be displayed as is 
     * without truncation of white spaces and new lines when reaching the end of the screen.
     * 
     * @param text The text to display
     * @param align The current horizontal alignment
     */
    private void showPreTagText(String text,int align) {
        if ((text==null) || (text.equals(""))) {
            return; //no text to show
        }

        String line="";
        
        for(int c=0;c<text.length();c++) {
            char ch=text.charAt(c);
            if (ch=='\n') {
                addString(line, align);
                newLine(align);
                line="";
            } else {
                line+=ch;
            }

        }
    }

    private Vector getWords(String text,int align,boolean returnComps) {
        Vector words=new Vector();
        String word="";
        for(int c=0;c<text.length();c++) {
            char ch=text.charAt(c);
            if ((ch==' ') || (ch==10) || (ch==13) || (ch=='\t') || (ch=='\n')) {
                if (!word.equals("")) {
                    if (returnComps) {
                        words.addElement(addString(word+' ', align));
                    } else {
                        words.addElement(word);
                    }
                    word="";
                }
            } else if ((!returnComps) && (font.stringWidth(word+ch)>width-leftIndent)) { //break words that are longer than the component's width
                   words.addElement(word);
                    word=""+ch;
            } else {
                word+=ch;
            }
        }
        if (!word.equals("")) {
            if (returnComps) {
                words.addElement(addString(word, align));
            } else {
                words.addElement(word);
            }
        }

        return words;
    }

    /*
     * This method will be used once FlowLayout is fixed
     */
    private Vector showText(String text,int align) {
        return getWords(text, align, true);
    }


    /**
     * Shows the given text. This method breaks lines as necessary and adds the text either as regular labels or links.
     *
     * @param text The text to display
     * @param align The current horizontal alignment
     */
    private Vector showTextFixedWidth(String text,int align) {
            Vector comps = new Vector();
            if ((text==null) || (text.equals(""))) {
                return comps; //no text to show
            }
            int spaceW=width-x;

            Vector words=getWords(text,align,false);

            if (words.size()>0) {
                int w=0;
                String wordStr="";
                if ((Parser.isWhiteSpace(text.charAt(0))) && (curLine.getComponentCount()!=0)) { //leading space is trimmed if it is in the first component of the line
                    wordStr=" "; //leading space
                }

                while (w<words.size()) {
                    String nextWord=(String)words.elementAt(w);
                    String space="";
                    if ((!wordStr.equals("")) && (!wordStr.equals(" "))) {
                        space=" ";
                    }
                    if (font.stringWidth(wordStr+space+nextWord)>spaceW-2) {
                        comps.addElement(addString(wordStr,align));
                        newLineIfNotEmpty(align);
                        spaceW=width-x;
                        wordStr=nextWord;
                    } else {
                        wordStr+=space+nextWord;
                    }
                    w++;
                }
                if (Parser.isWhiteSpace(text.charAt(text.length()-1))) {
                    wordStr+=" "; //trailing space
                }

                comps.addElement(addString(wordStr,align));
            }

            return comps;
    }

    /**
     * Adds the given text to the container as a label or a link.
     * The string given here does not need line breaking as this was calculated before in the calling method.
     * 
     * @param str The text to display
     * @param align The current horizontal alignment
     */
    private Label addString(String str,int align) {
        Label lbl=null;
        int color=textColor;

        if (link!=null) {
            lbl=new HTMLLink(str,link,this,mainLink);
            color=linkColor;
            if (linkVisited) {
                color=COLOR_VISITED_LINKS;
            }

            lbl.getSelectedStyle().setFont(font.getFont());
            if (mainLink==null) {
                mainLink=(HTMLLink)lbl;
            }
            if (accesskey!='\0') {
                addAccessKey(accesskey, lbl);//accessKeys.put(new Integer(accesskey), lbl);
            }
            lbl.getSelectedStyle().setMargin(0,0,0,0);
            lbl.getSelectedStyle().setPadding(0,0,0,0);

        } else {
            if (labelForID!=null) {
                lbl=new ForLabel(str, this, labelForID);
                if (accesskey!='\0') {
                    addAccessKey(accesskey, lbl);//accessKeys.put(new Integer(accesskey), lbl);
                }
            } else {
                lbl=new Label(str);
            }

        }
        lbl.getStyle().setMargin(0,0,0,0);
        lbl.getStyle().setPadding(0,0,0,0);
        lbl.getUnselectedStyle().setFgColor(color);
        lbl.getSelectedStyle().setFgColor(color);

        //lbl.setVerticalAlignment(Component.BOTTOM); //TODO - This still doesn't align as label alignment refers to the text alignment in relation to its icon (if exists)
        lbl.getUnselectedStyle().setFont(font.getFont());
        lbl.setGap(0);
        lbl.setTickerEnabled(false);
        lbl.setEndsWith3Points(false);
        if (align!=JUSTIFY) { // Regular LWUIT labels do not support justify. We acheive justification in fixed width mode below
            lbl.setAlignment(align);
        }

        lbl.getUnselectedStyle().setBgTransparency(0);

        curLine.addComponent(lbl);

        if (anchor!=null) {
            anchors.put(anchor,lbl);

        }

        if (FIXED_WIDTH) {
            if (align!=Component.LEFT) {
                if (align==JUSTIFY) {  // Text justification algorithm
                    Vector words=getWords(str,align,false);
                    if (words.size()>1) {
                        int spaceW=font.getFont().stringWidth(" ");
                        int spacesToAdd=(width-lbl.getPreferredW())/spaceW;
                        int spacesPerWord=spacesToAdd/(words.size()-1);
                        int addtlSpaces=spacesToAdd%(words.size()-1);
                        String newStr=(String)words.elementAt(0);
                        for(int i=1;i<words.size();i++) {
                            for(int j=0;j<spacesPerWord;j++) {
                                newStr+=' ';
                            }
                            if (i<=addtlSpaces) {
                                newStr+=' ';
                            }
                            newStr+=' '+(String)words.elementAt(i);
                        }
                        lbl.setText(newStr);
                    }
                } else {
                    lbl.setPreferredW(width);
                }
                x=width;
                newLine(align);
            } else {
                x+=lbl.getPreferredW();
            }
        }

        return lbl;

    }

    void addAccessKey(int accessKey,Component cmp) {
        accessKeys.put(new Integer(accessKey),cmp);
        Form form=getComponentForm();
        if (form!=null) {
            form.addKeyListener(accessKey,this);
        }
    }

    protected void initComponent() {
        super.initComponent();
        for (Enumeration e=accessKeys.keys();e.hasMoreElements();) {
            int keyCode=((Integer)e.nextElement()).intValue();
            getComponentForm().addKeyListener(keyCode,this);
        }
    }

    /**
     * Handles the IMG tag. This includes calculating its size (if available), applying any links/accesskeys and adding it to the download queue
     *
     * @param imgElement the IMG element
     * @param align th current alignment
     * @param cmd The submit command of a form, used only for INPUT type="image"
     */
    private void handleImage(Element imgElement,int align,Command cmd) {

            String imageUrl = imgElement.getAttributeById(Element.ATTR_SRC);
            Label imgLabel=null;
            if (imageUrl!=null) {

                String alignStr=imgElement.getAttributeById(Element.ATTR_ALIGN);

                    // Image width and height
                    int iWidth=calcSize(getWidth(), imgElement.getAttributeById(Element.ATTR_WIDTH),0);
                    int iHeight=calcSize(getHeight(), imgElement.getAttributeById(Element.ATTR_HEIGHT),0);

                    //Padding
                    int hspace=getInt(imgElement.getAttributeById(Element.ATTR_HSPACE));
                    int vspace=getInt(imgElement.getAttributeById(Element.ATTR_VSPACE));

                    int totalWidth=iWidth+hspace*2;

                    if ((FIXED_WIDTH) && (x+totalWidth>=width)) {
                        newLine(align);
                    }

                    // Alternative image text, shown until image is loaded.
                    String altText=imgElement.getAttributeById(Element.ATTR_ALT);

                    if (link!=null) { // This image is inside an A tag with HREF attribute
                        imgLabel=new HTMLLink(altText,link,this,mainLink);
                        if (mainLink==null) {
                            mainLink=(HTMLLink)imgLabel;
                        }
                        if (accesskey!='\0') {
                            addAccessKey(accesskey, imgLabel);//accessKeys.put(new Integer(accesskey), imgLabel);
                        }
                        ((Button)imgLabel).getPressedStyle().setPadding(vspace+1, vspace+1, hspace+1, hspace+1);

                    } else if (cmd!=null) { //Special case of an image submit button
                        imgLabel=new Button(cmd);
                        if ((altText!=null) && (!altText.equals(""))) {
                            imgLabel.setText(altText);
                        }
                        if (firstFocusable==null) {
                            firstFocusable=imgLabel;
                        }

                    } else {
                        imgLabel=new Label(altText);
                    }

                    imgLabel.setPreferredSize(new Dimension(iWidth,iHeight));
                    imgLabel.getSelectedStyle().setPadding(vspace, vspace, hspace, hspace);
                    imgLabel.getUnselectedStyle().setPadding(vspace, vspace, hspace, hspace);

                    imgLabel.getUnselectedStyle().setBorder(Border.createLineBorder(1));
                    imgLabel.getSelectedStyle().setBorder(Border.createLineBorder(1));

                    curLine.addComponent(imgLabel);
                    x+=totalWidth;

                    //Alignment
                    imgLabel.setAlignment(getHorizAlign(alignStr,align,false));
                    imgLabel.setVerticalAlignment(getVertAlign(alignStr,Component.CENTER));

                    if (showImages) {
                        if (docInfo!=null) {
                            imageUrl=docInfo.convertURL(imageUrl);
                        }
                        threadQueue.add(imgLabel, imageUrl);
                    }

                    if (loadCSS) {
                        imgElement.setAssociatedComponents(imgElement);
                    }

            }
    }

    /**
     * Handles the INPUT tag
     * 
     * @param element The input element
     * @param align The current aligment
     */
    private void handleInput(Element element,int align) {
        String type=element.getAttributeById(Element.ATTR_TYPE);
        if (type==null) {
            return;
        }
        int typeID=INPUT_TYPES.indexOf(type.toLowerCase());

        String name=element.getAttributeById(Element.ATTR_NAME);
        String id=element.getAttributeById(Element.ATTR_ID);
        String value=element.getAttributeById(Element.ATTR_VALUE);
        if (typeID==-1) {
            return;
        }
        if (value==null) {
            value="";
        }

        Component cmp=null;
        switch(typeID) {
            case INPUT_CHECKBOX:
                CheckBox cb=new CheckBox();
                if (element.getAttributeById(Element.ATTR_CHECKED)!=null) {
                    cb.setSelected(true);
                }
                cmp=cb;
                if (curForm!=null) {
                    curForm.addCheckBox(name, cb,value);
                }
                break;
            case INPUT_HIDDEN:
                if (curForm!=null) {
                    curForm.addInput(name, value,null);
                }
                break;
            case INPUT_TEXT:
            case INPUT_PASSWORD:
                TextField tf = new TextField(value);
                if (typeID==INPUT_PASSWORD) {
                    tf.setConstraint(TextField.PASSWORD);
                }

                if (SUPPORT_INPUT_FORMAT) {
                    HTMLInputFormat inputFormat=HTMLInputFormat.getInputFormat(element.getAttributeById(Element.ATTR_FORMAT));
                    if (inputFormat!=null) {
                        tf=(TextField)inputFormat.applyConstraints(tf);
                        if (curForm!=null) {
                            curForm.setInputFormat(tf, inputFormat);
                        }
                    }
                }

                int size=getInt(element.getAttributeById(Element.ATTR_SIZE));
                int maxlen=getInt(element.getAttributeById(Element.ATTR_MAXLENGTH));

                if (size==0) {
                    size=DEFAULT_TEXTFIELD_SIZE;
                }

                if (maxlen!=0) {
                    tf.setMaxSize(maxlen);
                    if (size>maxlen) {
                        size=maxlen;
                    }
                }

                tf.setPreferredW(tf.getStyle().getFont().stringWidth("W")*size);
                tf.getSelectedStyle().setFont(font.getFont());
                tf.getUnselectedStyle().setFont(font.getFont());
                
                /*
                 * BlueWhaleSystems fix - Michael Busheikin, 4/5/10
                 * Tint the background of the element when unselected so it isn't invisible. 
                 */
                if( 0xFFFFFF == tf.getUnselectedStyle().getBgColor() )
                {
                    tf.getUnselectedStyle().setBgColor( 0xD7ECFA );
                }
                
                cmp=tf;
                if (curForm!=null) {
                    curForm.addInput(name, cmp,value);
                }
                break;
            case INPUT_RADIO:
                RadioButton rb=new RadioButton(" ");
                if (element.getAttributeById(Element.ATTR_CHECKED)!=null) {
                    rb.setSelected(true);
                }

                cmp=rb;
                if (curForm!=null) {
                    curForm.addRadioButton(name, rb,value);
                }
                break;
            case INPUT_BUTTON: // No support for input type button, as these make sense only with javascript
                break;
            case INPUT_RESET:
                Command resetCmd=null;
                if (curForm!=null) {
                    if (!value.equals("")) {
                        curForm.setResetText(value);
                    }
                    resetCmd=curForm.getResetCommand();
                }
                if (resetCmd==null) {
                    resetCmd=new Command(HTMLForm.getDefaultResetText()); //dummy command - no form so it won't do anything
                }
                Button resetButton=new Button(resetCmd);
                cmp=resetButton;
                
                break;
            case INPUT_SUBMIT:
                Command submitCmd=null;
                if (curForm!=null) {
                    if (!value.equals("")) {
                        curForm.setSubmitText(value);
                    }
                    /*
                     * BlueWhaleSystems fix - Michael Busheikin, 11/6/10
                     * Submit button name and value should be sent with the POST parameters. 
                     */
                    submitCmd=new NamedCommand(value, curForm, true, name, value);
                    
                    curForm.hasSubmitButton=true;
                }
                if (submitCmd==null) {
                    submitCmd=new Command(HTMLForm.getDefaultSubmitText()); //dummy command - no form so it won't do anything
                }
                Button submitButton=new Button(submitCmd);
                cmp=submitButton;
                break;
            case INPUT_IMAGE: // Image submit is not officially supported in XHTML-MP 1.0 but was added anyway, but pixel data submission is not supported (i.e. name.x=xx&name.y=yy)
                submitCmd=null;
                if (curForm!=null) {
                    submitCmd=curForm.getSubmitCommand();
                    curForm.hasSubmitButton=true;
                }
                handleImage(element, align,submitCmd);
                break;
            case INPUT_FILE:
                //TODO - issue a parsing error???, perhaps catch in Parser
                break;
        }

        if (cmp!=null)  {
            String aKey=element.getAttributeById(Element.ATTR_ACCESSKEY);
            if ((aKey!=null) && (aKey.length()==1)) {
                addAccessKey(aKey.charAt(0), cmp);//accessKeys.put(new Integer(aKey.charAt(0)), cmp);
            }
            if (loadCSS) {
                element.setAssociatedComponents(cmp);
            }
            if ((curForm!=null) && (curForm.action==null)) { //Form that submits to a forbidden link
                cmp.setEnabled(false);
            } else if (firstFocusable==null) {
                firstFocusable=cmp;
            }

            if (id!=null) {
                inputFields.put(id,cmp);
            }
        }

        addCmp(cmp,align);

    }

    /**
     * Adds the given component to the curLine container after performing size checks
     * 
     * @param cmp The component to add
     */
    private void addCmp(Component cmp,int align) {
        if (cmp!=null) {
            if ((FIXED_WIDTH) && (x+cmp.getPreferredW()>width)) {
                newLine(align);
            }
            curLine.addComponent(cmp);
            x+=cmp.getPreferredW();
        }
    }

    /**
     * Updates the current margin with the given delta
     * 
     * @param delta The pixels to increment (positive value) or decrement (negative value) from the current margin
     */
    private void updateMargin(int delta) {
        leftIndent+=delta;
        x+=delta;
        curLine.getStyle().setMargin(Component.LEFT, leftIndent);
    }


    /**
     * Handles a single table cell (a TD tag)
     *
     * @param tdTag The TD tag element
     * @param align The current alignment
     */
    private void handleTableCell(Element tdTag,int align) {
            newLineIfNotEmpty(align);
            tableCells.addElement(curContainer);
            Container cell=new Container();
            cell.setLayout(new BoxLayout(BoxLayout.Y_AXIS));
            
            int border=0;
            Element trTag=tdTag.getParent();
            if (trTag!=null) { // Null checks to prevent exceptions for a TD tag without table etc.
                Element tableTag=trTag.getParent();
                if (tableTag!=null) {
                    border=getInt(tableTag.getAttributeById(Element.ATTR_BORDER));
                }
            }
            cell.getUnselectedStyle().setPadding(border, border, border, border);
            cell.getSelectedStyle().setPadding(border, border, border, border);

            Constraint constraint = new Constraint();

            int halign=getHorizAlign(tdTag.getParent().getAttributeById(Element.ATTR_ALIGN),align,false); // Get the default TR alignment
            int valign=getVertAlign(tdTag.getParent().getAttributeById(Element.ATTR_VALIGN),Component.CENTER); // Get the default TR valignment
            halign=getHorizAlign(tdTag.getAttributeById(Element.ATTR_ALIGN),halign,false);
            valign=getVertAlign(tdTag.getAttributeById(Element.ATTR_VALIGN),valign);
            int colspan=getInt(tdTag.getAttributeById(Element.ATTR_COLSPAN));
            int rowspan=getInt(tdTag.getAttributeById(Element.ATTR_ROWSPAN));
            String cWidth=tdTag.getAttributeById(Element.ATTR_WIDTH);
            int pW=getPercentage(cWidth);
            if ((pW>0) && (pW<100)) {
                //constraint.setWidthPercentage(pW); //TODO - Setting a width constraint currently makes the field width 0 - needs to be fixed in TableLayout
            } else {
                pW=getInt(cWidth);
                if (pW!=0) {
                    cell.setPreferredW(pW);
                }
            }
            String cHeight=tdTag.getAttributeById(Element.ATTR_HEIGHT);
            int pH=getPercentage(cHeight);
            if ((pH>0) && (pH<100)) {
                //constraint.setHeightPercentage(pH); //TODO - Setting a height constraint currently makes the field height 0 - needs to be fixed in TableLayout
            } else {
                pH=getInt(cHeight);
                if (pH!=0) {
                    cell.setPreferredH(pH);
                }
            }

            constraint.setHorizontalAlign(halign);
            constraint.setVerticalAlign(valign);

            //TODO - re-enable spanning code currently causes exceptions
            /*if (colspan>1) {
                constraint.setHorizontalSpan(colspan);
            }
            if (rowspan>1) {
                constraint.setVerticalSpan(rowspan);
            }*/

            curContainer=cell;
            if (curTable!=null) {
                curTable.addCell(cell,(tdTag.getId()==Element.TAG_TH),constraint);
            }

            if (loadCSS) {
                tdTag.setAssociatedComponents(cell);
            }

    }

    /**
     * Processes the given tag. This is the main processing method that calls all others and uses itself in a recursive manner.
     * 
     * @param element The element to process
     * @param align The current alignment 
     */
    private void processTag(Element element,int align) {
        if ((cancelled) && (!cancelledCaught)) {
            return;
        }
        int curAlign=align;

        HTMLFont oldFont=font;
        for(int i=0;i<element.getNumChildren();i++) {
            if ((cancelled) && (!cancelledCaught)) {
                break;
            }
            Element child=element.getChildAt(i);
            
            // Process Tag Open
            switch (child.getId()) {
                case Element.TAG_TEXT:
                    String text=child.getAttributeById(Element.ATTR_TITLE);
                    if ((curComboBox!=null) && (optionTag)) { // Text is inside an OPTION tag, i.e. belongs to a ComboBox
                        OptionItem oi = new OptionItem(text, optionValue);
                        curComboBox.addItem(oi);
                        if (optionSelected) {
                            curComboBox.setSelectedItem(oi);
                            if (curForm!=null) {
                                curForm.setDefaultValue(curComboBox, oi);
                            }
                        }
                    } else if (curTextArea!=null) { // Text is inside of a TEXTAREA tag
                        curTextArea.setText(text);
                        if (curForm!=null) {
                            curForm.setDefaultValue(curTextArea,text);
                        }
                    } else {
                      if (preTagCount!=0) {
                          showPreTagText(text, curAlign);
                      } else {
                          Vector comps=null;
                          if (FIXED_WIDTH) {
                            comps=showTextFixedWidth(text, curAlign);
                          } else {
                            comps=showText(text, curAlign);
                          }
                          if (loadCSS) {
                              child.setAssociatedComponents(comps);
                          }
                      }
                    }
                    break;
                case Element.TAG_A:
                    link=child.getAttributeById(Element.ATTR_HREF);
                    if ((link!=null) && (htmlCallback!=null)) {
                        int linkProps=htmlCallback.getLinkProperties(this, convertURL(link));
                        if ((linkProps & HTMLCallback.LINK_FORBIDDEN)!=0) {
                            link=null;
                        } else if ((linkProps & HTMLCallback.LINK_VISTED)!=0) {
                            linkVisited=true;
                        }
                    }

                    anchor=child.getAttributeById(Element.ATTR_NAME);

                    if (link!=null) {
                        String aKey=child.getAttributeById(Element.ATTR_ACCESSKEY);
                        if ((aKey!=null) && (aKey.length()==1)) {
                            accesskey=aKey.charAt(0);
                        }
                    }
                    break;
                case Element.TAG_H1:
                case Element.TAG_H2:
                case Element.TAG_H3:
                case Element.TAG_H4:
                case Element.TAG_H5:
                case Element.TAG_H6:
                    font=(HTMLFont)fonts.get(child.getName());
                    if (font==null) {
                        font=oldFont;
                    }
                    // No break here intentionally
                case Element.TAG_P:
                    curAlign=getHorizAlign(child.getAttributeById(Element.ATTR_ALIGN),align,true);
                    adjustAlignment(align, curAlign);
                    newLineIfNotEmpty(curAlign);
                    newLineIfLastWasNotEmpty(curAlign);
                    pushContainer(child);
                    break;
                case Element.TAG_DIV: 
                    curAlign=getHorizAlign(child.getAttributeById(Element.ATTR_ALIGN),align,true);
                    adjustAlignment(align, curAlign);
                    newLineIfNotEmpty(curAlign);
                    pushContainer(child);
                    break;
                case Element.TAG_FIELDSET:
                    newLineIfNotEmpty(curAlign);
                    Container newCont=new Container();
                    newCont.setUIID("HTMLFieldset");
                    if (fieldsets.size()==0) { // First fieldset shouldn't have margin
                        newCont.getStyle().setMargin(Component.LEFT,0);
                    }
                    newCont.setLayout(new BoxLayout(BoxLayout.Y_AXIS));
                    curContainer.addComponent(newCont);
                    fieldsets.addElement(curContainer);
                    curContainer=newCont;
                    if (loadCSS) {
                        child.setAssociatedComponents(newCont);
                    }

                    break;
                case Element.TAG_BR:
                    if (loadCSS) {
                        child.setAssociatedComponents(curLine);
                    }
                    newLine(curAlign);
                    break;
                case Element.TAG_DL:
                    newLineIfNotEmpty(curAlign);
                    newLine(curAlign);
                    pushContainer(child);
                    break;
                case Element.TAG_DT:
                    newLineIfNotEmpty(curAlign);
                    pushContainer(child);
                    break;
                case Element.TAG_UL:
                    newLineIfNotEmpty(curAlign);
                    ulLevel++;
                    updateMargin(INDENT_UL);
                    if ((ulLevel==1) && (olIndex==Integer.MIN_VALUE)) { //newline only if it's the first list
                        newLine(curAlign);
                    }
                    pushContainer(child);
                    break;
                case Element.TAG_OL:
                    newLineIfNotEmpty(curAlign);
                    if (olIndex!=Integer.MIN_VALUE) {
                        String indexStr=ORDERED_LIST_TYPE_IDENTIFIERS[listType]+""+olIndex;
                        olUpperLevelIndex.addElement(indexStr); //new Integer(olIndex));
                    }
                    olIndex=getInt(child.getAttributeById(Element.ATTR_START),1); //olIndex=1;
                    listType=getOrderedListType(child);
                    updateMargin(INDENT_OL);
                    if ((olUpperLevelIndex.size()==0) && (ulLevel==0)) { //newline only if it's the first list
                        newLine(curAlign);
                    }
                    pushContainer(child);
                    break;
                case Element.TAG_LI:
                    pushContainer(child);
                    if (child.getParent().getId()==Element.TAG_OL) {
                          olIndex=getInt(child.getAttributeById(Element.ATTR_VALUE),olIndex);
                          int itemListType=getOrderedListType(child,listType);
                          showTextFixedWidth(getListIndexString(olIndex, itemListType)+". ",align);
                    } else {
                          addCmp(new HTMLBullet(getUnorderedListType(child,ulLevel), font.getFont().getHeight(),textColor),curAlign);
                    }
                    break;

                case Element.TAG_BLOCKQUOTE:
                    newLineIfNotEmpty(curAlign);
                    updateMargin(INDENT_BLOCKQUOTE);
                    newLine(curAlign);
                    pushContainer(child);
                    break;
                case Element.TAG_DD:
                    newLineIfNotEmpty(curAlign);
                    updateMargin(INDENT_DD);
                    pushContainer(child);
                    break;
                case Element.TAG_HR:
                    newLineIfNotEmpty(curAlign);
                    Label hr=new Label();
                    hr.setUIID("HTMLHR");
                    //hr.getStyle().setBorder(Border.createBevelRaised());
                    int hrWidth=calcSize(width, child.getAttributeById(Element.ATTR_WIDTH), width);
                    int hrHeight=getInt(child.getAttributeById(Element.ATTR_SIZE), HR_THICKNESS);
                    hr.setPreferredW(hrWidth);
                    hr.setPreferredH(hrHeight);
                    curLine.addComponent(hr);
                    newLine(curAlign);
                    if (loadCSS) {
                        child.setAssociatedComponents(hr);
                    }
                    break;
                case Element.TAG_IMG:
                    handleImage(child,curAlign,null);
                    break;

                case Element.TAG_PRE:
                    preTagCount++;
                    pushContainer(child);
                case Element.TAG_EM:
                case Element.TAG_STRONG:
                case Element.TAG_DFN:
                case Element.TAG_CODE:
                case Element.TAG_SAMP:
                case Element.TAG_KBD:
                case Element.TAG_VAR:
                case Element.TAG_CITE:
                    font=(HTMLFont)fonts.get(child.getName());
                    if (font==null) {
                        font=oldFont;
                    }
                    break;

                case Element.TAG_B:
                case Element.TAG_I:
                case Element.TAG_BIG:
                case Element.TAG_SMALL:
                    font=getCounterpartFont(child.getId(), font);
                    break;
                case Element.TAG_FORM:
                    curForm=new HTMLForm(this,child.getAttributeById(Element.ATTR_ACTION),child.getAttributeById(Element.ATTR_METHOD),child.getAttributeById(Element.ATTR_ENCTYPE));
                    pushContainer(child);
                    break;
                case Element.TAG_INPUT:
                    handleInput(child,curAlign);
                    break;
                case Element.TAG_SELECT:
                    String multi = child.getAttributeById(Element.ATTR_MULTIPLE);

                    if (multi!=null) {
                        curComboBox=new MultiComboBox(true);
                        Container comboCont=new Container(new BorderLayout());
                        curComboBox.setItemGap(0);
                        comboCont.setUIID("ComboBox");
                        curComboBox.setUIID("List");
                        comboCont.addComponent(BorderLayout.CENTER,curComboBox);
                    } else {
                        curComboBox=new HTMLComboBox();
                    }
                    String name = child.getAttributeById(Element.ATTR_NAME);

                    if (curForm!=null) {
                        curForm.addInput(name,curComboBox,null);
                    }
                    if (loadCSS) {
                        child.setAssociatedComponents(curComboBox);
                    }
                    break;
                case Element.TAG_OPTGROUP:
                    if (curComboBox!=null) {
                        String label=child.getAttributeById(Element.ATTR_LABEL);
                        if (label!=null) {
                            curComboBox.addItem(label);
                        }
                    }
                    break;
                case Element.TAG_OPTION:
                    optionTag=true;
                    optionValue = child.getAttributeById(Element.ATTR_VALUE);
                    if ((curComboBox!=null) && (child.getAttributeById(Element.ATTR_SELECTED)!=null)) {
                        optionSelected=true;
                    }
                    break;
                case Element.TAG_TEXTAREA:
                    curTextArea = new TextArea(getInt(child.getAttributeById(Element.ATTR_ROWS),DEFAULT_TEXTAREA_ROWS),
                            getInt(child.getAttributeById(Element.ATTR_COLS),DEFAULT_TEXTAREA_COLS));

                    addCmp(curTextArea,curAlign);
                    if (loadCSS) {
                        child.setAssociatedComponents(curTextArea);
                    }
                    String aKey=element.getAttributeById(Element.ATTR_ACCESSKEY);
                    if ((aKey!=null) && (aKey.length()==1)) {
                        addAccessKey(aKey.charAt(0), curTextArea);//accessKeys.put(new Integer(aKey.charAt(0)), curTextArea);
                    }

                    break;
                case Element.TAG_Q:
                    addQuote(curAlign);
                    quoteTagCount++;
                    break;
                case Element.TAG_TABLE:
                    newLineIfNotEmpty(curAlign);
                    if (curTable!=null) {
                        tables.addElement(curTable);
                        HTMLTableModel newTable = new HTMLTableModel();
                        curTable=newTable;
                    } else {
                        curTable=new HTMLTableModel();
                    }
                    width=width/2; // TODO - this is temporary until issues with layout managers are solved. The idea is that we arbitrarily divide the size by a factor knowing that probably there are several cells

                    break;
                case Element.TAG_TR:
                    break;
                case Element.TAG_TH:
                case Element.TAG_TD:
                    if (curTable!=null) {
                        handleTableCell(child,curAlign);
                    }
                    break;
               case Element.TAG_LABEL:
                   labelForID=child.getAttributeById(Element.ATTR_FOR);
                   aKey=child.getAttributeById(Element.ATTR_ACCESSKEY);
                   if ((aKey!=null) && (aKey.length()==1)) {
                       accesskey=aKey.charAt(0);
                   }
                   break;

            }

            if (child.getNumChildren()>0) {
                processTag(child,curAlign);
            }

            // Process close tag
            switch(child.getId()) {
                case Element.TAG_H1:
                case Element.TAG_H2:
                case Element.TAG_H3:
                case Element.TAG_H4:
                case Element.TAG_H5:
                case Element.TAG_H6:
                    font=oldFont;
                case Element.TAG_P:
                    curAlign=align; //Restore previous alignment
                    newLineIfNotEmpty(curAlign);
                    popContainer();
                    newLine(curAlign);
                    break;
                case Element.TAG_DIV:
                    curAlign=align; //Restore previous alignment
                    newLineIfNotEmpty(curAlign);
                    popContainer();
                    break;
                case Element.TAG_FIELDSET:
                    newLineIfNotEmpty(curAlign);
                    Container upperContainer=(Container)fieldsets.lastElement();
                    //upperContainer.addComponent(curContainer);
                    curContainer=upperContainer;
                    fieldsets.removeElement(curContainer);
                    break;
                case Element.TAG_BLOCKQUOTE:
                    newLineIfNotEmpty(curAlign);
                    newLine(curAlign);
                    updateMargin(-INDENT_BLOCKQUOTE);
                    popContainer();
                    break;
                case Element.TAG_DT:
                    popContainer();
                    break;
                case Element.TAG_DD:
                    newLineIfNotEmpty(curAlign);
                    updateMargin(-INDENT_DD);
                    popContainer();
                    break;
                case Element.TAG_DL:
                    newLine(curAlign);
                    popContainer();
                    break;
                case Element.TAG_A:
                    link=null;
                    linkVisited=false;
                    mainLink=null;
                    anchor=null;
                    accesskey='\0';
                    break;
                case Element.TAG_UL:
                    ulLevel--;
                    if ((ulLevel==0) && (olIndex==Integer.MIN_VALUE)) {
                        newLine(curAlign);
                    }
                    updateMargin(-INDENT_UL);
                    popContainer();
                    break;
                case Element.TAG_OL:
                    if (olUpperLevelIndex.size()!=0) {
                        String indexStr=(String)olUpperLevelIndex.lastElement();
                        olUpperLevelIndex.removeElementAt(olUpperLevelIndex.size()-1);
                        listType=getOrderedListType(indexStr.charAt(0),LIST_NUMERIC);
                        olIndex=getInt(indexStr.substring(1));
                    } else {
                        olIndex=Integer.MIN_VALUE;
                    }
                    if ((olIndex==Integer.MIN_VALUE) && (ulLevel==0)) {
                        newLine(curAlign); //new line only if it is the last nested list
                    }
                    updateMargin(-INDENT_OL);
                    popContainer();

                    break;
                case Element.TAG_LI:
                    if (olIndex!=Integer.MIN_VALUE) {
                        olIndex++;
                    }
                    newLine(curAlign);
                    popContainer();
                    break;


                case Element.TAG_PRE:
                    preTagCount--;
                    popContainer();
                case Element.TAG_EM:
                case Element.TAG_STRONG:
                case Element.TAG_DFN:
                case Element.TAG_CODE:
                case Element.TAG_SAMP:
                case Element.TAG_KBD:
                case Element.TAG_VAR:
                case Element.TAG_CITE:
                case Element.TAG_B:
                case Element.TAG_I:
                case Element.TAG_BIG:
                case Element.TAG_SMALL:
                    font=oldFont;
                    break;
                case Element.TAG_FORM:
                    if ((curForm!=null) && (!curForm.hasSubmitButton) && (curForm.getNumFields()>0)) { // This is a fix for forms with no submit buttons which can be resulted due to the fact XHTML-MP doesn't support the BUTTON tag and also input type button with javascript
                        Button submitButton=new Button(curForm.getSubmitCommand());
                        addCmp(submitButton,curAlign);
                        curForm.hasSubmitButton=true;
                    }
                    curForm=null;
                    popContainer();
                    break;
                case Element.TAG_TEXTAREA:
                    String name = child.getAttributeById(Element.ATTR_NAME);

                    if (curForm!=null) { // This was moved to the end tag to enable auto complete support (i.e. if there's an autocomplete it overrides the default value)
                        curForm.addInput(name,curTextArea,null);
                    }

                    curTextArea=null;
                    break;
                case Element.TAG_SELECT:
                    if (curComboBox instanceof MultiComboBox) {
                        Container comboCont=curComboBox.getParent();
                        int minSize=Math.min(MIN_MULTI_COMBOBOX_ITEMS, curComboBox.size());
                        int maxSize=Math.min(curComboBox.size(),MAX_MULTI_COMBOBOX_ITEMS);
                        int size=Math.min(maxSize,Math.max(getInt(child.getAttributeById(Element.ATTR_SIZE)),minSize));

                        Component renderCmp=curComboBox.getRenderer().getListCellRendererComponent(curComboBox, "X", 0, false);
                        comboCont.setPreferredH((renderCmp.getPreferredH()+renderCmp.getStyle().getMargin(Component.TOP)+renderCmp.getStyle().getMargin(Component.BOTTOM)+curComboBox.getItemGap())*size
                                +curComboBox.getStyle().getPadding(Component.TOP)+curComboBox.getStyle().getPadding(Component.BOTTOM));
                        comboCont.setPreferredW(100);

                        addCmp(comboCont, curAlign);
                    } else {
                        addCmp(curComboBox, curAlign);
                    }

                    curComboBox=null;
                    break;
                case Element.TAG_OPTION:
                    optionTag=false;
                    optionSelected=false;
                    optionValue=null;
                    break;
                case Element.TAG_Q:
                    quoteTagCount--;
                    addQuote(curAlign);
                    break;
                case Element.TAG_TABLE:
                    newLineIfNotEmpty(curAlign);
                    curTable.commitRowIfNotEmpty(); // For a case that TR was not closed properly
                    if (curTable.getRowCount()!=0) { //Don't add an empty table (Creates an exception in TableLayout and useless)
                        HTMLTable table=new HTMLTable(curTable);
                        if (loadCSS) {
                            child.setAssociatedComponents(table);
                        }
                        
                        int border=getInt(child.getAttributeById(Element.ATTR_BORDER));
                        if (border>0) {
                            table.getUnselectedStyle().setBorder(Border.createLineBorder(border));
                            table.getSelectedStyle().setBorder(Border.createLineBorder(border));
                            table.getUnselectedStyle().setPadding(border, border, border, border);
                            table.getSelectedStyle().setPadding(border, border, border, border);
                        } else {
                            table.getUnselectedStyle().setBorder(null);
                            table.getSelectedStyle().setBorder(null);
                            table.setDrawBorder(false);
                        }

                        addCmp(table,curAlign);
                        newLineIfNotEmpty(curAlign);
                    }

                    if (tables.size()==0) {
                        curTable=null;
                    } else {
                        curTable=(HTMLTableModel)tables.lastElement();
                        tables.removeElement(curTable);
                    }
                    width=width*2; // TODO - this is temporary until issues with layout managers are solved.
                    if (width>displayWidth) {
                        width=displayWidth;
                    }
                    break;
                case Element.TAG_TR:
                    if (curTable!=null) {
                        curTable.commitRow();
                    }
                    break;
                case Element.TAG_TH:
                case Element.TAG_TD:
                    if (curTable!=null) {
                        newLineIfNotEmpty(curAlign);
                        curContainer=(Container)tableCells.lastElement();
                        tableCells.removeElement(curContainer);
                    }
                    break;
               case Element.TAG_LABEL:
                   labelForID=null;
                   accesskey='\0';
                   break;


            }

        }
    }

    /**
     * The following replaces the layout of the curLine container with the correct alignment, in non fixed width alignment is done by flowlayout's orientation.
     * In case the line is not empty, a new line will be opened by newLineIfNotEmpty below
     *
     * @param align The general alignment of the element
     * @param curAlign The alignment of the specific component we want to
     */
    private void adjustAlignment(int align,int curAlign) {
        if ((!FIXED_WIDTH) && (align!=curAlign)) {
            if (curLine.getComponentCount()==0) {
                curLine.setLayout(new FlowLayout(curAlign));
            }
        }
    }

    /**
     * Figures out what is the appropriate list type (i.e. which bullet types) for the unordered list in question
     *
     * @param element The element containing the UL tag
     * @param defaultType The default list type
     * @return The UL list type
     */
    private int getUnorderedListType(Element element,int defaultType) {
        String listTypeStr=element.getAttributeById(Element.ATTR_TYPE);
        if (listTypeStr==null) {
            return defaultType;
        }

        if (listTypeStr.equalsIgnoreCase("disc")) {
            return BULLET_DISC;
        } else if (listTypeStr.equalsIgnoreCase("circle")) {
            return BULLET_CIRCLE;
        } else if (listTypeStr.equalsIgnoreCase("square")) {
            return BULLET_SQUARE;
        }
        return defaultType;
    }

    /**
     * Figures out the appropriate list type for the given Ordered List element according to its attributes
     * This calls the getOrderedListType(Element,int) with LIST_NUMERIC as the default type
     *
     * @param element The OL element
     * @return The OL list type
     */
    private int getOrderedListType(Element element) {
        return getOrderedListType(element, LIST_NUMERIC);
    }
    
    /**
     * Figures out the appropriate list type for the given Ordered List element according to its attributes
     *
     * @param element The OL element
     * @param defaultListType The default list type
     * @return The OL list type
     */
    private int getOrderedListType(Element element,int defaultListType) {
        String listTypeStr=element.getAttributeById(Element.ATTR_TYPE);
        if ((listTypeStr!=null) && (listTypeStr.length()>0)) {
            char c=listTypeStr.charAt(0);
            return getOrderedListType(c, defaultListType);
        }
        return defaultListType;
    }

    /**
     * Figures out the appropriate list type for the given list type identifier
     * 
     * @param c The list identifier (one of ORDERED_LIST_TYPE_IDENTIFIERS) 
     * @param defaultListType The default list type
     * @return The OL list type
     */
    private int getOrderedListType(char c,int defaultListType) {
        for(int j=0;j<ORDERED_LIST_TYPE_IDENTIFIERS.length;j++) {
            if (c==ORDERED_LIST_TYPE_IDENTIFIERS[j]) {
                return j;
            }
        }
        return defaultListType;
    }

    /**
     * Returns a list index text according to the list type
     * 
     * @param index The list index
     * @param type The list type
     * @return The index converted according to the list style
     */
    private String getListIndexString(int index,int type) {
        if (index<=0) {
            return index+"";
        }
        switch(type) {
            case LIST_NUMERIC:
                return index+"";
            case LIST_UPPERCASE:
                return getLiteral(index,'A');
            case LIST_LOWERCASE:
                return getLiteral(index,'a');
            case LIST_ROMAN_UPPER:
                return getRomanIndexString(index);
            case LIST_ROMAN_LOWER:
                return getRomanIndexString(index).toLowerCase();
        }
        return "";
    }

    private String getLiteral(int index,char baseChar) {
        String literal="";
        while (index>0) {
            literal=(char)((index%26)+baseChar-1)+literal;
            index=index/26;
        }

        return literal;
    }

    /**
     * Converts a number to a roman numeral (Up to 99, should suffice for HTML lists...)
     *
     * @param index The list index to convert
     * @return A roman numeral representing the given index
     */
    private String getRomanIndexString(int index) {
        index=index%100;
        return ROMAN_NUMERALS_TENS[index/10]+ROMAN_NUMERALS_ONES[index%10];

    }

    private void pushContainer(Element element) {
        if (loadCSS) {
            Container cont=new Container(new BoxLayout(BoxLayout.Y_AXIS));
            cont.setScrollableX(false);
            //cont.getStyle().setBgColor(Element.COLOR_VALS[contCount]);
            //contCount=(contCount+1)%Element.COLOR_VALS.length; // debug for CSS
            //cont.getStyle().setBgTransparency(128);
            cont.getStyle().setBgTransparency(0);
            element.setAssociatedComponents(cont);
            curContainer.addComponent(cont);
            containers.addElement(curContainer);
            curContainer=cont;
        }
    }

    private void popContainer() {
        if (loadCSS) {
            Container prevContainer=(Container)containers.lastElement();
            curContainer=prevContainer;
            containers.removeElement(curContainer);
        }
    }

    /**
     * Adds a quote according to the current quote count 
     * 
     * @param curAlign The current horizontal alignment
     */
    private void addQuote(int curAlign) {
        String quote=null;
        if (quoteTagCount==0) { 
            quote="\"";
        } else {
            quote="'";
        }
        if ((FIXED_WIDTH) && (width-x<font.stringWidth(quote))) {
            newLine(curAlign);
        }
        addString(quote, curAlign);
    }


    /**
     * Converts a textual horizontal alignment description to a LWUIT alignment constant
     * 
     * @param alignment The string describing the alignment
     * @param defaultAlign The default alignment if the string cannot be converted
     * @param allowJustify true to allow justify alignment, false to return center if justify is mentioned
     * @return Component.LEFT, RIGHT or CENTER or the defaultAlign in case no match was found.
     */
    private int getHorizAlign(String alignment,int defaultAlign,boolean allowJustify) {
        if (alignment!=null) {
            if (alignment.equals("left")) {
                return Component.LEFT;
            } else if (alignment.equals("right")) {
                return Component.RIGHT;
            } else if ((alignment.equals("center")) || (alignment.equals("middle")))  {
                return Component.CENTER;
            } else if (alignment.equals("justify")) {
                return ((allowJustify) && (FIXED_WIDTH))?JUSTIFY:Component.CENTER;
            }
        }

        return defaultAlign; //unknown alignment

    }

    /**
     * Converts a textual vertical alignment description to a LWUIT alignment constant
     *
     * @param alignment The string describing the alignment
     * @param defaultAlign The default alignment if the string cannot be converted
     * @return Component.TOP, BOTTOM or CENTER or the defaultAlign in case no match was found.
     */
    private int getVertAlign(String alignment,int defaultAlign) {
        if (alignment!=null) {
            if (alignment.equals("top")) {
                return Component.TOP;
            } else if (alignment.equals("bottom")) {
                return Component.BOTTOM;
            } else if ((alignment.equals("center")) || (alignment.equals("middle")))  {
                return Component.CENTER;
            }
        }
        return defaultAlign;

    }

    /**
     * A convenience method to convert a string to an int.
     * This is mainly intended to save the try-catch for NumericFormatExceptions
     * 
     * @param intStr The string describing the integer
     * @param defaultValue The value to return if the string is not numeric
     * @return the integer value of the string, or defaultValue if the string is not numeric
     */
    private int getInt(String intStr,int defaultValue) {
        try {
            int num=Integer.parseInt(intStr);
            return num;
        } catch (NumberFormatException nfe) {
            return defaultValue;
        }
    }

    /**
     * A convenience method to convert a string to an int.
     * This is mainly intended to save the try-catch for NumericFormatExceptions
     *
     * @param intStr The string describing the integer
     * @return the integer value of the string, or 0 if the string is not numeric
     */
    private int getInt(String intStr) {
        return getInt(intStr,0);
    }

    /**
     * A convenience method to convert a string describing a percentage to an int.
     * 
     * @param percent The string representing the percentage
     * @return The percentage integer value (i.e. 80% will return 80) or 0 if the string is not a percentage
     */
    private int getPercentage(String percent) {
        if ((percent==null) || (!percent.endsWith("%"))) {
            return 0;
        }
        return getInt(percent.substring(0, percent.length()-1));

    }

    /**
     * Calculates width or height of an element according to its original size, requested size and default size
     * 
     * @param origDim The original width/height
     * @param requestedDim The string describing the requested width/height either in pixels or in percentage
     * @param defaultDim The default width/height to return in case the calculation fails
     * @return The new width/height according to the parameters
     */
    private int calcSize(int origDim,String requestedDim,int defaultDim) {
        if (requestedDim==null) {
            return defaultDim;
        }
        boolean percent=false;

        if (requestedDim.endsWith("%")) {
            percent=true;
            requestedDim=requestedDim.substring(0, requestedDim.length()-1);
        } else if (requestedDim.endsWith("px")) { // Pixels can be described either simply as '20' or as '20px'
            requestedDim=requestedDim.substring(0, requestedDim.length()-2);
        }

        int dim=0;
        try {
            dim=Integer.parseInt(requestedDim);
        } catch (Exception e) {
            dim=-1;
        }

        if (dim<0) { //Dimension was negative, or non-number
            return origDim;
        }

        if (percent) {
            return origDim*dim/100;
        } else {
            return dim;
        }
    }

    /**
     * Returns the input fields of this HTMLComponent, used by the FOR label mecahnism.
     * 
     * @return the input fields of this HTMLComponent
     */
    Hashtable getInputFields() {
        return inputFields;
    }

    /**
     * Focuses on the given component and if it's a checkbox/radiobutton selects it
     * This is used both for ForLabels and for access keys
     *
     * @param cmp The component to focus and select
     */
    void selectComponent(Component cmp) {
        getComponentForm().setFocused(cmp);
        getComponentForm().scrollComponentToVisible(cmp);
        if (cmp instanceof RadioButton) {
            ((RadioButton)cmp).setSelected(true);
        } else if (cmp instanceof CheckBox) {
            CheckBox cb=((CheckBox)cmp);
            cb.setSelected(!cb.isSelected());
        }
    }

    /**
     * Converts the given URL to an absolute URL based on the current page's URL
     *
     * @param url The url to convert (Can be relative)
     * @return The absolute URL representing the given URL in relation to the current one.
     */
    String convertURL(String url) {
        if (docInfo!=null) {
            return docInfo.convertURL(url);
        } else {
            return url; // No conversion is possible if we don't have a DocumentInfo object (in cases of setBody from a string and not from a document address), absolute URLs should have no problem, and as for relative the RequestHandler will have to handle these cases
        }
    }

    /**
     * Jumps to the given anchor
     * 
     * @param anchorName The anchor to jump to
     */
    void goToAnchor(String anchorName) {
        Label anchorCmp=(Label)anchors.get(anchorName);
        if (anchorCmp!=null) {
            int cx=anchorCmp.getX();
            int cy=anchorCmp.getY();
            int h= getHeight();
            if (anchorCmp.getAbsoluteY()-getY()+h>getPreferredH()) {
                h=getPreferredH()-(anchorCmp.getAbsoluteY()-getY());
            }
            scrollRectToVisible(cx, cy, getWidth(), h, anchorCmp);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void layoutContainer() {
        if ((FIXED_WIDTH) && (displayWidth!=0) && (Display.getInstance().getDisplayWidth()!=displayWidth)) {
            new Thread() {
                public void run() {
                    cleanup();
                    rebuildPage(); //screen form factor changed - landscape/portrait
                }
            }.start();
        }
        super.layoutContainer();
    }

    /**
     * {@inheritDoc}
     */
    public void actionPerformed(ActionEvent evt) {
        if (getComponentForm().getFocused() instanceof TextField) {
            return;
        }
        int keyCode=evt.getKeyEvent();
        Object obj = accessKeys.get(new Integer(keyCode));
        if (obj!=null) {
            if (obj instanceof HTMLLink) {
                HTMLLink htmlLink = (HTMLLink)obj;
                htmlLink.actionPerformed(null);
            } else if (obj instanceof ForLabel) {
                ((ForLabel)obj).triggerAction();
            } else if (obj instanceof Component) {
                selectComponent((Component)obj);
            }
        }
    }
}
/**
 * A thread used to refresh or redirect to another page in X seconds
 *
 * @author Ofir Leitner
 */
class RedirectThread implements Runnable {

    int seconds;
    String url;
    HTMLComponent htmlC;
    boolean cancelled;

    public RedirectThread(HTMLComponent htmlC,int seconds,String url) {
        this.seconds=seconds;
        this.url=url;
        this.htmlC=htmlC;
    }

     public void run() {
        try {
            Thread.sleep(seconds*1000);
        } catch (InterruptedException ie) {
            System.out.println("Warning: Redirect/Refresh thread sleep interrupted, page may refresh sooner than expected.");
        }
        if (!cancelled) {
            boolean redirect=(url!=null);
            url=htmlC.convertURL(url);
            if (redirect) {
                htmlC.setPageStatus(HTMLCallback.STATUS_REDIRECTED);
            }

            htmlC.setPage(url); 
        }
    }

    public void cancel() {
        cancelled=true;
    }

}

/**
 * A label that when clicked focuses (and toggels when applicable) a certain input field.
 * This implements the LABEL html tag
 * 
 * @author Ofir Leitner
 */
class ForLabel extends Label {

    String id;
    HTMLComponent htmlC;

    public ForLabel(String labelText,HTMLComponent htmlC,String id) {
        super(labelText);
        this.id=id;
        this.htmlC=htmlC;
    }

    /**
     * {@inheritDoc}
     */
    public void pointerReleased(int x, int y) {
        triggerAction();
        super.pointerPressed(x, y);
    }

    /**
     * Triggers the needed action for this ForLabel
     */
    public void triggerAction() {
        Component cmp = (Component)htmlC.getInputFields().get(id);
        if (cmp!=null) {
            htmlC.selectComponent(cmp);
        }
    }
}

/**
 * A simple class used to hold both a display text and an actual value for a combobox item.
 * The renderer takes the display text, while the form submission process takes the value.
 *
 * @author Ofir Leitner
 */
class OptionItem {

    String text;
    String value;

    public OptionItem(String text,String value) {
        this.text=text;
        if (value==null) { // If no value was specified, the value is the display text
            this.value=text;
        } else {
            this.value=value;
        }
    }

    String getValue() {
        return value;
    }

    public String toString() {
        return text;
    }

}

/**
 * A simple class drawing a bullet in various styles
 * 
 * @author Ofir Leitner
 */
class HTMLBullet extends Component {

    int level;
    int fontHeight;
    int color;

    public HTMLBullet(int level,int fontHeight,int color) {
        this.level=level;
        this.fontHeight=fontHeight;
        this.color=color;
        getStyle().setBgTransparency(0);
        setFocusable(false);
    }

    /**
     * {@inheritDoc}
     */
    public void paint(Graphics g) {
        int size=fontHeight/3;
        g.setColor(color);
        if (level==1) { // Filled circle
            size+=2;
            g.fillArc(getX()+(getWidth()-size)/2, getY()+(getHeight()-size)/2, size,size,0,360);
        } else if (level==2) { // Empty circle
            g.drawArc(getX()+(getWidth()-size)/2, getY()+(getHeight()-size)/2, size,size,0,360);
        } else { // Filled rectangle
            g.fillRect(getX()+(getWidth()-size)/2, getY()+(getHeight()-size)/2, size,size);
        }
    }

    /**
     * {@inheritDoc}
     */
    protected Dimension calcPreferredSize() {
        return new Dimension(fontHeight,fontHeight);
    }

}

/**
 * HTMLComboBox overrides ComboBox to allow usage of MultiComboBox as its list.
 * This is done for OPTGROUP labels support (Note that multiple features of MultiComboBox will be switched off)
 *
 * @author Ofir Leitner
 */
class HTMLComboBox extends ComboBox {

    /**
     * {@inheritDoc}
     */
    protected List createPopupList() {
        List l = new MultiComboBox(getModel(),false);
        l.setSmoothScrolling(isSmoothScrolling());
        l.setFixedSelection(getFixedSelection());
        l.setItemGap(getItemGap());
        l.setUIID("ComboBoxList");
        return l;
    }

}

